TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

[bits 32]
section .text
;---------------------------put_char-------------------------
;function:把栈中的一个字符写入光标所在处
;------------------------------------------------------------
global put_char ;将put_char导出为全局符号,外部可见
put_char:
    pushad      ;备份32位寄存器环境 PUSH ALL DOUBLE 压入4*8=32字节
    ;需要保证gs中为正确的食品段选择子;为保险起见，每次打印时都为gs赋值
    mov ax, SELECTOR_VIDEO
    mov gs, ax

    ;获取当前光标位置
    ;高8位
    mov dx, 0x03d4      ;索引寄存器
    mov al, 0x0e        ;用于提供光表位置的高8位
    out dx, al
    mov dx, 0x03d5      ;通过读写数据端口0x03d5来获得或设置光标位置
    in al, dx           ;得到了光标位置的高8位
    mov ah, al

    ;低8位
    mov dx, 0x03d4      ;索引寄存器
    mov al, 0x0f        ;用于提供光表位置的低8位
    out dx, al
    mov dx, 0x03d5      ;通过读写数据端口0x03d5来获得或设置光标位置
    in al, dx           ;得到了光标位置的低8位

    ;光标存入bx
    mov bx, ax
    ;在栈中获取待打印的字符
    mov ecx, [esp + 36] ;pushad压入了32字节,加上主调函数4字节的返回地址
    cmp cl, 0xd         ;CR回车是0x0d
    jz .is_carriage_return
    cmp cl, 0xa         ;LF换行符是0x0a
    jz .is_line_feed    
    cmp cl, 0x8         ;BS(backspace)的asc码是8
    jz .is_backspace

    jmp .put_other


.is_backspace:
;;;;;;;;;;;;backspace 的一点说明;;;;;;;;;;
; 当为 backspace 时，本质上只要将光标移向前一个显存位置即可.后面再输入的字符自然会覆盖此处的字符
; 但有可能在键入 backspace 后并不再键入新的字符，这时光标已经向前移动到待删除的字符位置，但字符还在原处
; 这就显得好怪异，所以此处添加了空格或空字符 0
    dec bx              ;在此之前bx是下一个字符打印位置,这里要回到上一个待删除字符去
    shl bx, 1           ;光标左移1为,即乘2
                        ;表示光标对应显存中的偏移字节
    mov byte [gs:bx], 0x20  ;将待删除的字节补为0或空格
    inc bx
    mov byte [gs:bx], 0x07  ;黑屏白字
    shr bx, 1               ;除2取整,恢复光标
    jmp .set_cursor

.put_other:
    shl bx, 1
    mov [gs:bx], cl
    inc bx
    mov byte [gs:bx], 0x07
    shr bx, 1               ;恢复到先前字符的坐标
    inc bx                  ;下一个坐标,新的可以打印字符的坐标
    cmp bx, 2000            ;2000是一页中的最大,超过要滚屏
    jl .set_cursor          ;若光标值小于 2000，表示未写到显存的最后，则去设置新的光标值,若超出屏幕字符数大小（2000）则换行处理

.is_line_feed:              ;换行\n
.is_carriage_return:        ;回车\r
;如果是 CR(\r)，只要把光标移到行首就行了
    xor dx, dx              ;dx是被除数的高16位,清0
    mov ax, bx              ;ax是被除数的低16位
    mov si, 80              ;效仿linux,\n表示下一行行首,把\n和\r都处理为linux中\n的意思,即下一行行首
    div si                  ;对80求模,余数在dx中
    sub bx, dx              ;光标值减去除 80 的余数,便是当前的行首坐标


.is_carriage_return_end:    ;\r处理结束
    add bx, 80              ;换行
    cmp bx, 2000            ;滚屏判断
.is_line_feed_end:          ;\n处理结束
    jl .set_cursor          ;jl 小于时跳转

;屏幕行范围是0～24，滚屏的原理是将屏幕的第1～24行搬运到第0～23行,再将第24行用空格填充
.roll_screen:               ;若超出屏幕大小，开始滚屏
    cld
    mov ecx, 960            ;2000-80=1920个字符要搬运，共 1920*2=3840字节,一次搬4字节，共3840/4=960次
    mov esi, 0xc00b80a0     ;第1行行首(虚拟地址)
    mov edi, 0xc00b8000     ;第0行行首
    rep movsd

    ;将最后一行填充为空白
    mov ebx, 3840           ;最后一行首字符的第一个字节偏移=1920*2
    mov ecx, 80
.cls:
    mov word [gs:ebx], 0x0720   ;黑底白字的空格
    add ebx, 2
    loop .cls
    mov bx, 1920            ;将光标值重置为1920，最后一行的首字符

.set_cursor:
    ;将光标设为bx值
    ;高8位
    mov dx, 0x03d4      ;索引寄存器
    mov al, 0x0e        ;用于提供光表位置的高8位
    out dx, al
    mov dx, 0x03d5      ;通过读写数据端口0x03d5来获得或设置光标位置
    mov al, bh
    out dx, al          

    ;低8位
    mov dx, 0x03d4      ;索引寄存器
    mov al, 0x0f        ;用于提供光表位置的低8位
    out dx, al
    mov dx, 0x03d5      ;通过读写数据端口0x03d5来获得或设置光标位置
    mov al, bl
    out dx, al
.put_char_done:
    popad               ;POP ALL DOUBLE
    ret


;---------------------------put_str--------------------------
;function:打印以0字符结尾的字符串
;------------------------------------------------------------
;input :栈中参数为打印字符串
;output:无

global put_str
put_str:
    push ebx
    push ecx            ;只用到了这两个寄存器,故只备份2个
    xor ecx, ecx        ;准备用ecx存储参数
    mov ebx, [esp + 12] ;栈中的到待打印的字符串地址
.goon:
    mov cl, [ebx]
    cmp cl, 0           ;与ASCII码0比较,如果处理到了字符串尾,跳转到结束
    jz .str_over
    push ecx            ;为put_char函数传递参数
    call put_char
    add esp, 4          ;回收栈空间
    inc ebx             ;使ebx指向下一个字符
    jmp .goon
.str_over:
    pop ecx
    pop ebx
    ret


;---------------------------put_int--------------------------
;function:将小段字节序的数字变成对应的ASCII码后,倒置(打印16进制数字)
;------------------------------------------------------------
;input :栈中参数为打印数字
;output:在屏幕上打印16进制数字,不打印前缀0x
put_int_buffer dq 0     ;定义8字节缓冲区用于数字到字符的转换
global put_int
put_int:
    pushad
    mov ebp, esp
    mov eax, [ebp + 4*9];call的返回地址4字节+pushad的8个4字节
    mov edx, eax    
    mov edi, 7          ;指定在put_int_buffer中初始的偏移量
    mov ecx, 8          ;32位数字中,十六进制数字的位数是8个
    mov ebx, put_int_buffer

    ;将 32 位数字按照十六进制的形式从低位到高位逐个处理,共处理 8 个十六进制数字
.16based_4bits:         ;每4位二进制是16进制的1位,遍历每一位16进制数字
    and edx, 0x0000000f ;解析16进制数字的每一位,and后,edx只有低位有效
    cmp edx, 9          ;数字0-9和a-f分别处理为对应字符
    jg .is_A2F          ;edx>9
    add edx, '0'        ;加上字符'0'的ASCII码,edx低8位有效
    jmp .store
.is_A2F:
    sub edx, 10         ;A-F减去A(=10),再加上字符'A'的ASCII码
    add edx, 'A'

;将每一位数字转换成对应的字符后，按照类似“大端”的顺序,存储到缓冲区 put_int_buffer
;高位字符放在低地址，低位字符要放在高地址，这样和大端字节序,类似,只不过咱们这里是字符序
.store:
    ;此时dl中是数字对应的字符的ASCII码
    mov [ebx+edi], dl
    dec edi
    shr eax, 4
    mov edx, eax
    loop .16based_4bits

    ;现在 put_int_buffer 中已全是字符，打印之前
    ;把高位连续的字符去掉，比如把字符 000123 变成 123
.ready_to_print:
    inc edi             ;此时edi退减为-1(0xffffffff),加1使其为0
.skip_prefix_0:
    cmp edi, 8          
    je .full0           ;edi=8,表示已经比较第9个字符了
    ;找出连续的'0'字符,edi作为非0的最高位字符的偏移
.go_on_skip:
    mov cl, [put_int_buffer+edi]
    inc edi             ;指向下一个字符
    cmp cl, '0'
    je .skip_prefix_0   ;edi-1处为0,继续判断edi处
    dec edi             ;edi-1不为0,edi已经加了1,要减去1,恢复指向
    jmp .put_each_num

.full0:
    mov cl, '0'         ;只打印0
.put_each_num:
    push ecx            ;cl中为可打印字符
    call put_char
    add esp, 4
    inc edi             ;指向下一个字符
    mov cl, [put_int_buffer+edi]
    cmp edi, 8
    jl .put_each_num    ;edi<8时继续打印
    popad
    ret

;---------------------------set_cursor--------------------------
;function:设置光标
;------------------------------------------------------------
;input :栈中参数为打印数字
;output:在屏幕上打印16进制数字,不打印前缀0x
global set_cursor
set_cursor:
    pushad
    mov ax, SELECTOR_VIDEO
    mov gs, ax

    mov ebp, esp
    mov ebx, [ebp + 4*9];call的返回地址4字节+pushad的8个4字节
    ;将光标设为bx值
    ;高8位
    mov dx, 0x03d4      ;索引寄存器
    mov al, 0x0e        ;用于提供光表位置的高8位
    out dx, al
    mov dx, 0x03d5      ;通过读写数据端口0x03d5来获得或设置光标位置
    mov al, bh
    out dx, al          

    ;低8位
    mov dx, 0x03d4      ;索引寄存器
    mov al, 0x0f        ;用于提供光表位置的低8位
    out dx, al
    mov dx, 0x03d5      ;通过读写数据端口0x03d5来获得或设置光标位置
    mov al, bl
    out dx, al

    popad
    ret

;---------------------------cls_screen--------------------------
;function:清屏
;------------------------------------------------------------
;input :无
;output:无
global cls_screen
cls_screen:
    pushad
    mov ax, SELECTOR_VIDEO
    mov gs, ax

    mov ebx, 0
    mov ecx, 80*25

.cls:
    mov word [gs:ebx], 0x0720   ; 黑底白字的空格
    add ebx, 2
    loop .cls
    mov ebx, 0

.ser_cursor:
    ;高8位
    mov dx, 0x03d4      ;索引寄存器
    mov al, 0x0e        ;用于提供光表位置的高8位
    out dx, al
    mov dx, 0x03d5      ;通过读写数据端口0x03d5来获得或设置光标位置
    mov al, bh
    out dx, al          

    ;低8位
    mov dx, 0x03d4      ;索引寄存器
    mov al, 0x0f        ;用于提供光表位置的低8位
    out dx, al
    mov dx, 0x03d5      ;通过读写数据端口0x03d5来获得或设置光标位置
    mov al, bl
    out dx, al

    popad
    ret